#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# vim:set fileencoding=utf8 shiftwidth=4 expandtab textwidth=72:
"""
This module defines classes to improve timezone support for builtin
datetime module.

Datetime module supports timezone information by tzinfo abstract class.
However Python library does not have any pre-defined timezone classes.

TaggyTodo aims to work with two time systems: UTC time and local time.
We defines two new classes: UTC and LocalTZ, which are derived from
datetime.tzinfo class. User can set the tzinfo properties of created
datetime objects to make them timezone awared.
"""

__all__ = [ 'UTCTZ', 'LocalTZ', 'now', 'utcnow', 'isdatetime',
            'istzaware', 'isutctime', 'ltou', 'utol', 'today',
            'utctoday', 'ldatetime', 'utcdatetime', 'daytostr',
            'strtolday', 'strtoutcday' ]

import time
import datetime
import os

# XXX The definitions below come from sample code in Python 2.6
# documents, but with some modifications. Check the original definitions
# from here:
#
#     http://docs.python.org/library/datetime.html
#
ZERO = datetime.timedelta(0)

class UTCTZ(datetime.tzinfo):
    """UTC timezone"""
    def utcoffset(self, dt):
        return ZERO
    def tzname(self, dt):
        return "UTC"
    def dst(self,dt):
        return ZERO

class LocalTZ(datetime.tzinfo):
    """
    Timezone definitions based on current system timezone settings. 

    In POSIX environments, users can create different timezone objects
    by chaning TZ environment variable, just as shown by the code below:

        import os
        os.environ['TZ'] = ':Asia/Shanghai'
        time.tzset()
        shanghaiTZ = LocalTZ()
    """
    def __init__(self):
        datetime.tzinfo.__init__(self)
        # XXX The time.timezone property returns the offset between 
        # current timezone and UTC, in seconds. Timezones in west always
        # return positive numbers, which means it is behind UTC. For
        # timezones in east, it returns negative numbers.
        STDOFFSET = datetime.timedelta(seconds = -time.timezone)
        if time.daylight:
            DSTOFFSET = datetime.timedelta(seconds = -time.altzone)
        else:
            DSTOFFSET = STDOFFSET
        DSTDIFF = DSTOFFSET - STDOFFSET
        self.__STDOFFSET = STDOFFSET
        self.__DSTOFFSET = DSTOFFSET
        self.__DSTDIFF   = DSTDIFF

    def utcoffset(self, dt):
        if self._isdst(dt):
            return self.__DSTOFFSET
        else:
            return self.__STDOFFSET
    def dst(self, dt):
        if self._isdst(dt):
            return self.__DSTDIFF
        else:
            return ZERO
    def tzname(self, dt):
        return time.tzname[self._isdst(dt)]
    def _isdst(self, dt):
        ttuple = (dt.year, dt.month, dt.day, dt.hour, dt.minute,
                  dt.second, dt.weekday(), 0, 1)
        stamp = time.mktime(ttuple)
        tlocal = time.localtime(stamp)
        return tlocal.tm_isdst > 0
    # The following properties are not part of tzinfo definitions.
    @property
    def DSTOFFSET(self):
        return self.__DSTOFFSET
    @property
    def STDOFFSET(self):
        return self.__STDOFFSET
    @property
    def DSTDIFF(self):
        return self.__DSTDIFF

# global functions. They are used as wrappers of standard datetime
# objects, but with tzinfo defined.
utc = UTCTZ()
startup_localtz = LocalTZ()
def now():
    # XXX It might cause a big memory overhead by creating a lot of
    # small objects.
    return datetime.datetime.now(LocalTZ())
def utcnow():
    return datetime.datetime.utcnow().replace(tzinfo = UTCTZ())

def isdatetime(dt):
    "Check if the type of given object is datetime.datetime."
    return type(dt) is datetime.datetime
def istzaware(dt):
    "Check if given datetime object is timezone awared."
    return isdatetime(dt) and dt.tzinfo is not None
def isutctime(dt):
    "Check if given datetime object is calculated in UTC."
    if not istzaware(dt):
        return False
    tz = dt.tzinfo
    return tz.utcoffset(dt) == ZERO and tz.dst(dt) == ZERO

def ltou(dt):
    if not istzaware(dt):
        raise ValueError("dt", "Not timezone awared")
    if isutctime(dt):
        return dt
    return dt.astimezone(UTCTZ())

def utol(dt):
    if not istzaware(dt):
        raise ValueError("dt", "Not timezone awared")
    if not isutctime(dt):
        return dt
    localtz = LocalTZ()
    return localtz.fromutc(dt.replace(tzinfo=localtz))

#
# The following functions defines the default routines for datetime
# calculation. They will be used by TaggTODO queries.
#
def today():
    n = now()
    return datetime.datetime(n.year, n.month, n.day, \
                             0, 0, 0, 0, n.tzinfo)  
def utctoday():
    n = utcnow()
    return datetime.datetime(n.year, n.month, n.day, \
                             0, 0, 0, 0, n.tzinfo)  
def year_firstday(dt):
    """
    year_firstday(dt) -> datetime object
    Return the first day of a year, specified by datetime object.
    """
    return datetime.datetime(dt.year, 1, 1, dt.hour, dt.minute, \
                             dt.second, dt.microsecond, dt.tzinfo)
def year_lastday(dt):
    """
    year_lastday(dt) -> datetime object
    Return the last day of a year, specified by datetime object.
    """
    return datetime.datetime(dt.year, 12, 31, dt.hour, dt.minute, \
                             dt.second, dt.microsecond, dt.tzinfo)
def year_delta(dt, delta):
    # XXX I can return only the first day of a month because the days in
    # a month varies.
    return datetime.datetime(dt.year + delta, dt.month, 1, \
                             dt.hour, dt.minute, dt.second, \
                             dt.microsecond, dt.tzinfo)

def month_firstday(dt):
    """
    month.firstday(dt) -> datetime object
    Return the first day of a month, specified by datetime object.
    """
    return datetime.datetime(dt.year, dt.month, 1, dt.hour, dt.minute, \
                             dt.second, dt.microsecond, dt.tzinfo)
def month_lastday(dt):
    """
    month.lastday(dt) -> datetime object
    Return the last day of a month, specified by datetime object.
    """
    month = dt.month
    year  = dt.year
    day = 0
    mod = month % 2
    if month == 2:
        if (year % 100 == 0 and year % 400 == 0) or year % 4 == 0:
            day = 29
        else:
            day = 28
    elif (mod == 0 and month <= 6) or (mod == 1 and month >= 9): 
        day = 30
    else:
        day = 31
    return datetime.datetime(year, month, day, dt.hour, dt.minute, \
                             dt.second, dt.microsecond, dt.tzinfo)
def month_delta(dt, delta):
    month = dt.month
    year = dt.year
    newyear = year
    newmonth = month + delta
    if newmonth > 12 or newmonth < 0:
        newyear += (newmonth / 12)
        newmonth %= 12
    # XXX
    # Given the month delta is not a strict definition, I can only return
    # the first day of new month, because I don't want to add more 
    # complexity to calculate the 'exact' date you want.
    return datetime(newyear, newmonth, 1, dt.hour, dt.minute, \
                             dt.second, dt.microsecond, dt.tzinfo) 

def week_firstday(dt):
    """
    week_firstday(dt) -> datetime object
    Return the first day of a week, specified by datetime object.
    """
    weekday = dt.weekday()
    if weekday == 0: # Monday is the first day, starting from 0
        return dt
    weekdelta = datetime.timedelta(days = weekday)
    return (dt - weekdelta)

def week_lastday(dt):
    """
    week_lastday(dt) -> datetime object
    Return the last day of a week, specified by datetime object.
    """
    weekday = dt.weekday()
    if weekday == 6: # Sunday is the last day, ending at 6
        return dt
    weekdelta = datetime.timedelta(days = (6 - weekday))
    return (dt + weekdelta)

def week_delta(dt, delta):
    weekdelta = datetime.timedelta(days = (7 * delta))
    return dt + weekdelta

def day_delta(dt, delta):
    daydelta = datetime.timedelta(days = delta)
    return dt + daydelta
def hour_delta(dt, delta):
    hourdelta = datetime.timedelta(hours = delta)
    return dt + hourdelta
def minute_delta(dt, delta):
    minutedelta = datetime.timedelta(minutes = delta)
    return dt + minutedelta

def ldatetime(year, month, day, hour = 0, minute = 0):
    localtz = LocalTZ()
    return datetime.datetime(year, month, day, hour, minute, 0, 0, localtz)

def utcdatetime(year, month, day, hour = 0, minute = 0):
    return datetime.datetime(year, month, day, hour, minute, 0, 0, UTCTZ())

def daytostr(dt):
    return "{0.year:04}/{0.month:02}/{0.day:02}".format(dt)
def dttostr(dt):
    return "{0.year:04}/{0.month:02}/{0.day:02}/{0.hour:02}/{0.minute:02}".format(dt)

def strtoutcday(str):
    dt_fields = str.split('/')
    if len(dt_fields) == 3:
        try:
            year  = int(dt_fields[0])
            month = int(dt_fields[1])
            day   = int(dt_fields[2])
            dt = utcdatetime(year, month, day)
            return dt
        except Exception:
            pass
    elif len(dt_fields) == 5:
        try:
            year   = int(dt_fields[0])
            month  = int(dt_fields[1])
            day    = int(dt_fields[2])
            hour   = int(dt_fields[3])
            minute = int(dt_fields[4])
            dt = utcdatetime(year, month, day, hour, minute)
            return dt
        except Exception:
            pass
    return None
def strtolday(str):
    dt_fields = str.split('/')
    if len(dt_fields) == 3:
        try:
            year  = int(dt_fields[0])
            month = int(dt_fields[1])
            day   = int(dt_fields[2])
            dt = ldatetime(year, month, day)
            return dt
        except Exception as e:
            pass
    elif len(dt_fields) == 5:
        try:
            year   = int(dt_fields[0])
            month  = int(dt_fields[1])
            day    = int(dt_fields[2])
            hour   = int(dt_fields[3])
            minute = int(dt_fields[4])
            dt = ldatetime(year, month, day, hour, minute)
            return dt
        except Exception as e:
            pass
    return None


# vim:tw=78:et:sw=4
